#include "portaudio_internal.hpp"

#include <cmath>
#include <gvl/support/cstdint.hpp>
#include <gvl/system/memory.hpp>
#include <vector>

static luaL_Reg portaudio[] =
{
	{NULL, NULL}
};

int counter = 0;

struct Sound
{
	Sound()
	: data(44100)
	{
		unsigned long middle = (unsigned long)(data.size() / 2);
		for(unsigned long i = 0; i < data.size(); ++i)
		{
			double ramp = i < middle ? i / double(middle) : (2*middle - i) / double(middle);
			int samp = int(std::sin(i * 6.28 * 440.0 / 44100) * 0.4 * 32768.0 * ramp);
			data[i] = samp;
		}
	}

	std::vector<int16_t> data;
};

struct Channel
{
	enum
	{
		Looping = (1<<0)
	};

	Channel()
	: sound(0)
	{
	}

	// Mutator read/write
	uint32_t id;

	// Mutator write once, mixer read
	uint32_t flags;

	// Mutator write once, mixer read
	Sound* sound;

	// Mutator write once, mixer read/write
	uint32_t pos;
};

int const channelCount = 16;

Sound testSound;

struct Mixer
{
	Mixer()
	: writePos(0), tailPos(0)
	, baseFrame(0), nextId(0)
	{
		for(uint32_t i = 0; i < channelCount; ++i)
			channels[i] = i;
	}

	uint32_t add(Sound* sound, uint32_t pos)
	{
		if(freeChannels() > 1)
		{
			uint32_t chIdx = channels[writePos];

			Channel& ch = channelStates[chIdx];
			ch.sound = sound;
			ch.flags = Channel::Looping;
			ch.pos = pos;

			uint32_t id = ((nextId++) << 16) | chIdx;
			ch.id = id;

			GVL_WRITE_SYNC();
			writePos = (writePos + 1) & (channelCount-1);

			return id;
		}

		return 0;
	}

	void swap(uint32_t idx1, uint32_t idx2)
	{
		std::swap(channels[idx1], channels[idx2]);
	}

	uint32_t activeChannels() const
	{
		GVL_READ_SYNC();
		return (writePos - tailPos) & (channelCount-1);
	}

	uint32_t freeChannels() const
	{
		return channelCount - activeChannels();
	}

	bool addChannel(int16_t* out, unsigned long frameCount, Channel& ch)
	{
		Sound* sound = ch.sound;
		uint32_t time = ch.pos;

		int32_t relbegin = time - baseFrame;
		int32_t relend = time - baseFrame + int32_t(sound->data.size());

		if(relbegin < 0)
			relbegin = 0;
		if(relend > int32_t(frameCount))
			relend = int32_t(frameCount);

		for(int32_t cur = relbegin; cur < relend; ++cur)
		{
			int32_t soundpos = cur + baseFrame - time;

			int32_t samp = out[cur];
			int32_t soundsamp = sound->data[soundpos];
			samp += soundsamp;
			if(samp < -32768)
				samp = -32768;
			else if(samp > 32767)
				samp = 32767;
			out[cur] = int32_t(samp);
		}

		return relend == int32_t(frameCount);
	}

	void mix(void *output, unsigned long frameCount)
	{
		int16_t* out = static_cast<int16_t*>(output);

		memset(out, 0, frameCount * sizeof(int16_t));

		uint32_t localWritePos = writePos;
	
		for(uint32_t c = tailPos; c != localWritePos; )
		{
			uint32_t chIdx = channels[c];
			Channel& ch = channelStates[chIdx];

			if(!addChannel(out, frameCount, ch))
			{
				swap(c, tailPos);
				tailPos = (tailPos + 1) & (channelCount-1);
			}

			c = (c + 1) & (channelCount-1);
		}

		baseFrame += frameCount;
	}

	uint32_t nextId;
	Channel channelStates[channelCount];

	uint32_t writePos, tailPos;
	uint32_t channels[channelCount];

	int32_t baseFrame;
};

Mixer mixer;

int callback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData)
{
	mixer.mix(output, frameCount);

	return paContinue;
}

int luaopen_portaudio(lua_State* L)
{
	// TODO: Make sure only called once and
	// make sure there's a matching Terminate call.
	Pa_Initialize();

	PaStream* str;
	PaError err = Pa_OpenDefaultStream(&str, 0, 1, paInt16, 44100.0, 0,
                              callback, // streamCallback
                              0);

	Pa_StartStream(str);

	mixer.add(&testSound, 44100 * 3);
	mixer.add(&testSound, 44100 * 3 + 22000);

	luaL_openlib(L, "portaudio", portaudio, 0);

	return 1;
}
